"""  Property Tree support for qt.

This module defines the classes needed to support a property tree interface
similar to those made popular in development IDEs for Java, python, and many
other systems.

This module contains:

  * Core Classes:
    * PropertyTreeView: Specialized QTreeView class for property tree
    * PropertyDelegate: Helper delegate for PropertyTreeView class.
    * PropertyTreeModel: Base model for the property tree.  Should work in most cases.   
    * IProperty: The interface that must be implemented for a property in the tree.

  * Property Hierarchy
    * PropertyContainer: Base of property containment hierarchy
    * PropertyGroup: Group of properties.
    * Property: An individual property

The property hierarchy classes do not have to be used, but they provide a recommended way
to structure your property tree data.  The idea of the hierarchy is that there
is a container that contains groups of properties and each of these groups contains
the actual properties.  These hierarchy classes implement the IProperty interface
to provide this grouping.   
"""
import os, sys, os.path
from PyQt4 import QtGui, QtCore

class PropertyTreeView(QtGui.QTreeView):
   """ Customized tree view class. """
   def __init__(self,parent):
      QtGui.QTreeView.__init__(self,parent)
      
      # Customize the tree
      self.setAlternatingRowColors(True)
      self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
      self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
      self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
      self.setRootIsDecorated(True)
      self.setEditTriggers(QtGui.QAbstractItemView.CurrentChanged|
                                    QtGui.QAbstractItemView.SelectedClicked)
      self.setIndentation(15)
      self.delegate = PropertyDelegate()
      self.setItemDelegate(self.delegate)      

   def drawBranches(self, painter, rect, index):
      opt = QtGui.QStyleOptionViewItem(self.viewOptions())
      
      item = index.internalPointer()

      # Draw background (separator or highlight)
      if item.isSeparator():
         painter.fillRect(rect, opt.palette.mid())
      elif self.selectionModel().isSelected(index):
         painter.fillRect(rect, opt.palette.brush(QtGui.QPalette.Highlight))
      
      #QtGui.QTreeView.drawBranches(self, painter, rect, index)
      # XXX: It would be nice to get rid of the connection lines in the expander.
      if self.model().hasChildren(index):
         opt.state = opt.state | QtGui.QStyle.State_Children
         if self.isExpanded(index):
            opt.state = opt.state | QtGui.QStyle.State_Open
                     
         prim = QtCore.QRect(rect.left(), rect.top(), self.indentation(), rect.height())
         opt.rect = prim
         self.style().drawPrimitive(QtGui.QStyle.PE_IndicatorBranch, opt, painter, self)      
      
      # Draw line at bottom of non-separator branches
      if not item.isSeparator():
         saved_pen = painter.pen()      
         grid_color = opt.palette.mid().color()
         grid_pen = QtGui.QPen(grid_color)
         painter.setPen(grid_pen)
         painter.drawLine(rect.x(), rect.bottom(), rect.right(), rect.bottom())
         painter.setPen(saved_pen)

   def setModel(self, model):
      QtGui.QTreeView.setModel(self, model)    # Call parent
      self._model = model
      self.connect(self._model, QtCore.SIGNAL("modelReset()"), self.onReset)
      self.onReset()
   
   def onReset(self):
      print "Model reset"
      prop_root = self._model.propRoot
      if prop_root:
         self.setRootIndex(self._model.indexOf(prop_root))
      else:
         print "No prop root set."

class PropertyDelegate(QtGui.QItemDelegate):
   """ Custom delegate to use with property tree.
       It in turn delegates all it's work to the property class found from the
       property tree model.
   """
   def __init__(self,parent=None):
      QtGui.QItemDelegate.__init__(self, parent)
   
   def createEditor(self, parent, option, index):
      """ Return widget for editing the given index. """
      assert index.isValid()
      item = index.internalPointer()
      if index.column() == 0 and item.hasNameEditor(index):
         return item.createNameEditor(parent,option,index)
      elif item.hasEditor(index):
         return item.createEditor(parent, option, index)
      else:
         return None
      
   def setEditorData(self, widget, index):
      item = index.internalPointer()
      if index.column() == 0:
         item.setNameEditorData(widget,index)
      else:
         item.setEditorData(widget,index)
   
   def setModelData(self, widget, model, index):
      item = index.internalPointer()
      if index.column() == 0:
         item.setNameModelData(widget, model, index)
      else:
         item.setModelData(widget, model,index)
   
   def updateEditorGeometry(self, editor, option, index):
      editor.setGeometry(option.rect)
   
   def paint(self, painter, opt, index):
      """ Customize the paint to draw the property editor in a more consistent way. """
      #print "delegate paint"
      item = None
      if index.isValid():
         item = index.internalPointer()
      option = QtGui.QStyleOptionViewItem(opt)    # Get copy of options

      # Separator and second column don't show selected
      if item.isSeparator() or index.column() == 1:
         option.state = option.state & ~QtGui.QStyle.State_Selected
      option.state = option.state & ~QtGui.QStyle.State_HasFocus
         
      # We are a separator, draw a background and disable selection
      if item.isSeparator():
         option.palette.setColor(QtGui.QPalette.Text, option.palette.color(QtGui.QPalette.BrightText))
         option.font.setBold(True)         
         
         #option.palette.setColor(QtGui.QPalette.Window, option.palette.color(QtGui.QPalette.Dark))
         bg = option.palette.mid()
         painter.fillRect(option.rect, bg)
      
      saved_pen = painter.pen()
      
      # Finish with default paint   
      QtGui.QItemDelegate.paint(self, painter, option, index)
      
      if not item.isSeparator():
         grid_color = option.palette.mid().color()
         grid_pen = QtGui.QPen(grid_color)
      
         painter.setPen(grid_pen)
         # Line at bottom of entry and line at right of entry
         painter.drawLine(option.rect.x(), option.rect.bottom(), option.rect.right(), option.rect.bottom())         
         painter.drawLine(option.rect.right(), option.rect.y(), option.rect.right(), option.rect.bottom())
      painter.setPen(saved_pen)


class PropertyTreeModel(QtCore.QAbstractItemModel):
   def __init__(self, parent=None):
      QtCore.QAbstractItemModel.__init__(self, parent)
      self.propRoot = None
      self.leaky_bucket = []       # Hold references to python objects so they stay alive      

   def makeIndex(self, row, col, obj):
      self.leaky_bucket.append(obj)
      return self.createIndex(row, col, obj)
   
   def setPropRoot(self, root):
      """ Set to a root node.  Reset the tree and data. """
      self.propRoot = root
      self.propRoot.setModel(self)
      self.leaky_bucket = []     # Clear out the bucket
      self.reset()               # Reset the model, triggers view reloads

   def isEditable(self, index):
      """ Return true if the index is editable. """
      if index.column() == 0 and index.internalPointer().hasNameEditor(index):
         return True
      elif index.internalPointer().hasEditor(index):
         return True 
      else:
         return False
      
   def flags(self, index):
      if not index.isValid():
         return QtCore.Qt.ItemIsEnabled
      
      ret_flags = QtCore.QAbstractItemModel.flags(self, index)
      if self.isEditable(index):
         ret_flags = ret_flags | QtCore.Qt.ItemIsEditable      
      return ret_flags

   def index(self, row, column, parentIdx):
      # If parent invalid, return highest level items
      if not parentIdx.isValid():
         if self.propRoot:
            return self.makeIndex(row,column,self.propRoot)
         else:
            return QtCore.QModelIndex()
      parent = parentIdx.internalPointer()
      
      return self.makeIndex(row, column, parent.getChild(row))
   

   def parent(self, childIdx):      
      # If invalid, or root, then return no parent
      if not childIdx.isValid():
         return QtCore.QModelIndex()

      child = childIdx.internalPointer()      
      assert child
      
      if self.propRoot == child:
         return QtCore.QModelIndex()
      
      return self.indexOf(child.getParent())
   
   def indexOf(self, item):
      """ Return the index of item relative to parent. """
      if item == self.propRoot:
         return self.createIndex(0, 0, self.propRoot)
      
      parent = item.getParent()
      row = parent.indexOf(item)
      return self.makeIndex(row, 0, item)
   
   def rowCount(self, parentIdx):
      if not parentIdx.isValid():
         return 1               # Root node
      else:
         parent_node = parentIdx.internalPointer()
         return parent_node.getNChildren()   
   
   def columnCount(self, parent):
      # Just have 1 column for now
      return 2      
   
   def buddy(self, index):
      """ When trying to edit index, which index should be edited (my buddy.) """
      item = index.internalPointer()      
      if index.column() == 0 and not item.hasNameEditor(index):
         return self.makeIndex(index.row(), 1, index.internalPointer())
      return index   # Default to being my buddy (column 1)

   def data(self, index, role):
      if not index.isValid():
         if role == QtCore.Qt.UserRole:
            return None
         else:
            return QtCore.QVariant()
         
      item = index.internalPointer()
      col = index.column()

      # Data
      if 0 == col:
         if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            return QtCore.QVariant(item.getName())
         elif role == QtCore.Qt.UserRole:
            return item
      elif 1 == col:
         if role == QtCore.Qt.DisplayRole:
            return QtCore.QVariant(item.toString())
         elif role == QtCore.Qt.EditRole:
            return QtCore.QVariant(item.toString())   # Not used (delegate gets it direct)
         elif role == QtCore.Qt.DecorationRole:
            return item.decoration()
      
      if role == QtCore.Qt.ToolTipRole:
         tooltip = item.tooltip()
         if type(tooltip) != QtCore.QVariant:
            tooltip = QtCore.QVariant(tooltip)
         return tooltip
      elif role == QtCore.Qt.StatusTipRole:
         statustip = item.statustip()
         if type(statustip) != QtCore.QVariant:
            statustip = QtCore.QVariant(statustip)
         return statustip      
      
      # Formatting
      if QtCore.Qt.FontRole == role:
         pass
         #print "Font"
      elif QtCore.Qt.BackgroundColorRole == role:
         pass
         #print "Background"
      
      # Catch all
      return QtCore.QVariant()
   
   def headerData(self, col, orientation, role):
      if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
         headers = ["Property","Value"]         
         return QtCore.QVariant(str(headers[col]))
      return QtCore.QVariant()

   def subTreeChanged(self, item=None):
      """ Reset to model because subtree at item changed. """
      if not item:
         self.reset()
      else:
         self.reset()
      

class IProperty(object):
   """ Base interface for all elements in the property tree.
   """
   def __init__(self):
      self.name = "<noname>"
      self.children = []      
      
   def getName(self):
      """ Get the name of the property.  Not specifically needed, but can be helpful. """
      return self.name
      
   def value(self):
      """ Return the value of the property in whatever form is usable for this prop type. """
      return "str"
   
   def toString(self):
      """ Return string representation of the property value.  This is used to display
          in the tree when not in editing mode.
      """
      return str(self.value())
   
   def decoration(self):
      """ Return the decoration to use for the property.  Defaults to nothing. """
      return QtCore.QVariant()
   
   def tooltip(self):
      """ Return text to use in tooltip. """
      return QtCore.QVariant()
   
   def statustip(self):
      """ Return text to use in status tip. """
      return QtCore.QVariant()
   
   def isSeparator(self):
      """ Return true if this property should be rendered as a "separator" in the tree.
          Normally only true for groups. """
      return False
   
   # Helper to get model
   def getModel(self):
      """ return the model we are using. """
      return self.getParent().getModel()
   
   # Parent/child interface
   def getChild(self, index):
      return self.children[index]
   def addChild(self, child):
      self.children.append(child)
   def getNChildren(self):
      return len(self.children)
   def indexOf(self, item):
      return self.children.index(item)
   def getParent(self):
      return None

   # Data (value) Editor interface
   def hasEditor(self,index):
      """ Return true if there is a custom editor for the delegate to use at the index.
          Returning true for col 0 allows the name to be editable.
      """
      return False

   def createEditor(self, parent, option, index):
      """ Return an editor to use in the delegate. """
      return None
   def setEditorData(self, widget, index):
      """ When passed a widget, set it up with the right data. """
      pass
   def setModelData(self, widget, model, index):
      """ Called after edit is complete.  Read data from widget into the item. """
      pass

   # --- Name editor interface --- #
   def hasNameEditor(self,index):
      """ Return true if there is a custom editor for the delegate to use at the index.
          Returning true for col 0 allows the name to be editable.
      """
      return False

   def createNameEditor(self, parent, option, index):
      """ Return an editor to use in the delegate. """
      return None
   def setNameEditorData(self, widget, index):
      """ When passed a widget, set it up with the right data. """
      pass
   def setNameModelData(self, widget, model, index):
      """ Called after edit is complete.  Read data from widget into the item. """
      pass


class PropertyContainer(IProperty):
   """ Property container.  
      Root of the tree.  Just holds property groups.  Should never been seen in the tree
   """
   def __init__(self, name):
      super(PropertyContainer,self).__init__()      
      self.name = name
      self.model = None
   
   def setModel(self, model):
      self.model = model
   def getModel(self):
      return self.model
   
   def toString(self):
      return self.name
   def value(self):
      return "no_value"
   def hasEditor(self,index):
      return False  
   def getParent(self):
      return None
   
class PropertyGroup(IProperty):
   """ A group of properties.
       Displays as a separator in the tree and contains properties to edit.
   """
   def __init__(self, name, parentProp):
      super(PropertyGroup,self).__init__()
      self.name = name
      self.parentProp = parentProp      

   def toString(self):
      return ""
   def value(self):
      return ""
   def hasEditor(self,index):
      return False
   def isSeparator(self):
      return True
   def getParent(self):
      return self.parentProp   

class Property(IProperty):
   """ Common base class for properties. """
   def __init__(self, name, propGroup):
      super(Property,self).__init__()
      self.name = name
      self.propGroup = propGroup      
   
   def getParent(self):
      return self.propGroup
      
   def hasEditor(self,index):
      return False

# Example property editors
class StringTestProperty(Property):
   """ Example of text only property. """
   def __init__(self, name, propGroup):
      super(StringTestProperty,self).__init__(name, propGroup)
      self.val = "string"
   def value(self):
      return self.val   

class FloatTestProperty(Property):
   def __init__(self, name, propGroup):
      """ Example of property with custom editor and setting. """
      super(FloatTestProperty,self).__init__(name, propGroup)
      self.val = 0.0

   def value(self):
      return self.val
   def hasEditor(self,index):
      return True
   def createEditor(self, parent, option, index):
      editor = QtGui.QSpinBox(parent)
      editor.setMinimum(0)
      editor.setMaximum(100)
      return editor
   def setEditorData(self, widget, index):
      widget.setValue(self.val)
   def setModelData(self, widget, model, index):
      self.val = widget.value()      
   def tooltip(self):
      return "Float tooltip"
   def statustip(self):
      return "Float statustip"



if __name__ == "__main__":   
   def buildPropData():
      cont = PropertyContainer("myCont")
      g1 = PropertyGroup("Group1",cont)
      g2 = PropertyGroup("Group2",cont)
      g3 = PropertyGroup("Group3",cont)      
      g4 = PropertyGroup("Group4",cont)
      
      for g in [g1,g2,g3,g4]:
         cont.addChild(g)
         g.addChild(StringTestProperty("prop1",g))
         g.addChild(FloatTestProperty("prop2",g))      
      return cont 

   class PropTestWidget(QtGui.QWidget):
      def __init__(self, parent=None):
         QtGui.QWidget.__init__(self, parent)
         self.setObjectName("PropTest")
         self.resize(QtCore.QSize(QtCore.QRect(0,0,300,500).size()).expandedTo(self.minimumSizeHint()))
         self.vbox = QtGui.QVBoxLayout(self)
         
         self.model = PropertyTreeModel(None)   
         self.model.setPropRoot(buildPropData())      
         
         self.custTreeView = PropertyTreeView(self)
         self.custTreeView.setModel(self.model)
         #self.custTreeView.setRootIndex(self.model.indexOf(self.model.propRoot))      
         self.vbox.addWidget(self.custTreeView)
   
   app = QtGui.QApplication(sys.argv)
   gui = PropTestWidget()   
   gui.show()
   sys.exit(app.exec_())
   

   